Skip to content

feat(spec): gate the liveness README's state table against GOVERNED (#7257) - #7374

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7257-liveness-readme-gate
Aug 10, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-7257-liveness-readme-gate

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7257

Option 3 as the spine, option 2 riding, option 1 by measurement only.

Premise check (on merged main, f188ed6)

PR #7255 landed earlier today: it moved the heading to 30 and added a note admitting the table carried 28 of them, deliberately not back-filling. That premise held exactly — before this change the section read ## Current state — 30 governed types (complete registry coverage) over 28 rows, with GOVERNED at 30. The new gate's first run against that tree reproduced it without being told:

✗ 2 governed type(s) with NO row in the README's "Current state" table:
    api
    capability
✗ 1 README state-table heading error(s):
    heading says 30 governed types, the table has 28 row(s)

#7255's admission note is replaced (not merely edited) — the paragraph that said "filed rather than back-filled from a guess" now records that the heading is checked and that the rows were back-filled from measurement, keeping its reasoning about why the Notes cell is the part a gate must not author.

(3) The gate — a fourth direction, wired the existing way

scripts/liveness/readme-table.mts: pure, unit-tested, imported by check-liveness.mts, exactly the shape of orphans.mts / drill.mts / producer.mts. No new check:/gen: script — it rides inside check:liveness, which spec-liveness-check.yml already runs on every PR touching packages/spec/**. check:generated --reconcile-only (the #4203 meta-gate) stays green with no ledger change: 20 check: + 14 gen: scripts, all classified.

It fails, not warns — the population is exact, there is no debt to amortise once the rows are back-filled, and this README's own verdict is that a permanently-noisy check is a check nobody reads. Four findings:

Finding Why
a GOVERNED type with no row #7257 itself
a row GOVERNED does not back the mirror — same rot as an orphan ledger row, one level up
the same type in two rows would let the row count reach GOVERNED.length while a real type has none
N ≠ row count, or N ≠ GOVERNED.length all three legs, because two of them agreeing is exactly the state this issue found

Unreadable table lines are reported, never skipped — a row the parser cannot see is a row it cannot govern, which is #4956 again. Fenced code blocks are skipped so the regeneration snippet's print(f"| {t} | …") template is not read as a row, and only the "Current state" section is parsed so the Status-vocabulary and proof tables above it don't inflate the count.

(2) The regeneration snippet prints a skeleton

The documented snippet now reads the table back as well as the report, so a governed type with no row prints a marked skeleton instead of silently not being printed next to 29 siblings that are. Both legs run:

$ … --json | python3 -c "…"     # on this branch: no skeletons, and
| api        | 25 | 0 | 0 | 2 |  # the counts match the rows written below
| capability | 12 | 0 | 0 | 0 |

$ …                              # against a README with the qa row removed:
| qa | 4 | 0 | 5 | 0 | **NO ROW YET (#7257) — write this Notes cell from the seeding PR measurement, never from a guess** |

Both were needed: the count columns get regenerated far more often than the row set gets audited. The skeleton stops at the count columns on purpose — it prints a marker where the Notes cell goes, never a guess at what belongs there.

(1) The two rows — counts from --json, Notes from the landing evidence

Counts are the gate's own report, run fresh (the #4488 method). Every clause of both Notes cells is quoted or paraphrased from a source below; nothing is inferred from the counts or the type name.

api — 25 / 0 / 0 / 2. Sources: liveness/api.json's own _note; issue #5206 (the defect); issue #5271 and its PM acceptance comment for PR #5312; the two transform entries' notes.

#5271 acceptance: "liveness 走路线 1 —— GOVERNED + 播种 liveness/api.json,27 键分类(live 25 / planned 2 / dead 0),每键 file:line 按消费层引证 … transform 判 planned 的理由成立(已被响亮拒绝的键不是 enforce-or-remove 候选)"

25 + 2 = 27 — the seeding measurement and today's --json agree exactly. #5206 measured the pre-fix state: api in neither DEFAULT_METADATA_TYPE_REGISTRY nor BUILTIN_METADATA_TYPE_SCHEMAS, so resolveOverlaySchema('api', …) → getMetadataTypeSchema('api') returned undefined and saveMetaItem took its documented "unregistered type → stored unvalidated" branch, while getMetaTypes() could not enumerate it. The planned verdict is quoted from the ledger, not restated:

inputMapping.transform: "PLANNED, deliberately not dead, and the difference is the point. dead in this ledger means 'parsed, no consumer' — a silent no-op. transform is the opposite: it is parsed and then LOUDLY REFUSED, at publish (endpoint-publish-gate.ts mappingGate) and again at runtime (:259), because there is no transformation-function registry anywhere in the platform."

capability — 12 / 0 / 0 / 0. Sources: liveness/capability.json's _note; issue #5961; PR #6540 (merged 2026-08-08, Fixes #5961).

PR #6540: "新建 liveness/capability.json(5 个可授权属性逐条 call-graph 闭合到真实读者,全 live,未走 PENDING_GOVERNANCE 记债)"

The gate's 12 and the PR's 5 are the same measurement at two granularities — 5 authorable properties plus the 7 ADR-0010 envelope keys the gate auto-classifies live (they carry null verdicts in the file, as on permission/position). The row says so, because the README's own caveat is that counts are at the gate's one-level walk granularity. PR #6540's reverse verification is recorded because it measured something sharper than it predicted:

"逆向两肢各自重建 dist 后跑:肢一删注册条目 7 红 3 绿(且发现比预判更锐的事实:垃圾载荷转 422 而非 resolve——schema 绑定是条目之后真实的第二道防线);肢二删 schema 绑定恰 3 红。"

Both types turn out to be the same worked example, and PR #6540 says so in those words ("enforced but undeclared,#5271 关掉那个洞的镜像") — the mirror of this ledger's usual declared ≠ enforced. Nothing in either cell is prose I supplied about somebody else's measurement.

Also fixed: a stray blank line between the view and report rows that had been splitting the table into two rendered tables.

Reverse verification — predicted first, and pinned in CI

Predicted before running: on the pre-backfill tree the gate exits 1 naming exactly api and capability plus one heading error (30 vs 28 rows), with no heading-vs-GOVERNED error (30 === 30), no orphan rows and no malformed lines. That is verbatim what it printed (above) — the two-of-three-legs-agreeing signature the issue describes.

Rather than perform it once, three cases in check-liveness.test.ts drive the real gate (--ledger-root= at a copy of the real ledgers, which cpSync carries README.md into) and read its exit code:

Mutation in the copy Result
delete the qa row exit 1, names qa, and asserts the "never from a guess" prescription survives with the check
heading → 99 governed types exit 1, both heading legs reported
add a notatype row exit 1, orphan direction
verbatim copy (the control) exit 0 — without it every "exit 1" above is also satisfied by an unreadable copy

Gates

Gate Result
check:liveness exit 0 — success line now ends "…and the README state table carries a row for each of the 30 governed type(s) it claims to index"
packages/spec vitest 363 files / 9491 tests passed
readme-table.test.ts + check-liveness.test.ts 31 passed (16 new)
check:scripts-typecheck exit 0
check:generated --reconcile-only (#4203) exit 0, ledger unchanged
check:empty-state, check:variant-docs, check:strictness-ledger exit 0
scripts/check-nul-bytes.mjs OK
CI on c3a3ace all 26 check-runs green (2 path-filtered skips: Console Pin Gate, Build Docs)

Heavy runs under flock /tmp/os-heavy-verify.lock.

Re-checked against main at 3c03725 (3 commits ahead of the branch point): merges clean, and check:liveness is green on the merged tree — verified in a throwaway worktree so this branch's green CI stays untouched. One incoming commit edits liveness/action.json, which changes no row of the table's row set and no GOVERNED entry.

Scope notes

…7257)

The ledger README's "Current state" table is the liveness ledger's own index —
one row per governed type, counts regenerated from the gate's --json report, a
hand-written Notes cell per row. Its heading, `## Current state — N governed
types (complete registry coverage)`, is a completeness CLAIM that nothing could
falsify: N was the count of ROWS, not of governed types, and the two agreed only
by coincidence. They stopped. `api` and `capability` were both in GOVERNED, both
had ledgers, both were counted by the gate, and neither had a row.

Same shape the file spends 500 lines warning about, one level up: `dashboard.
widgets` asserted in prose that its 22 child keys were classified in a subtree
that never existed, and survived a release because prose cannot fail a build
(#4956). Every other claim in the file became data the gate resolves; the index
was the last one riding on a human reading it.

- readme-table.mts — the fourth direction, pure and unit-tested for the same
  reason as orphans.mts: on a green tree the table is complete, so a passing gate
  proves nothing about whether the check can fire. Fails (not warns) on a missing
  row, an orphan row, a duplicated row, or a heading N that disagrees with either
  the row count or GOVERNED.length. All three heading legs, because two of them
  agreeing is exactly the state this issue found.
- No new check:/gen: script — it rides inside check:liveness, which the Spec
  property liveness workflow already runs on packages/spec/**. check:generated
  --reconcile-only stays green with no ledger change.
- The documented regeneration snippet now reads the table back as well as the
  report, so a governed type with no row prints a SKELETON row: the omission is
  visible at regeneration time as well as at CI time.
- The two missing rows are back-filled from their seeding PRs' own measurements —
  counts from --json, prose from what #5271/PR #5312 and #5961/PR #6540 actually
  measured. The Notes cell is the one thing the gate deliberately does not check,
  which is why they were filed rather than fixed on the spot.
- Also removes a stray blank line that had been splitting the table in two.

Reverse verification is pinned in CI, not just performed once: three cases in
check-liveness.test.ts drive the REAL gate against a copy of the real README with
a row deleted, the heading skewed, and an ungoverned row added, and read exit 1.

Closes #7257
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 8:10am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

⛔ 7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(liveness): the README state table has no rows for api / capability — the table silently fell behind its own registry

2 participants